﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Velocity4Net.Errors;
using Velocity4Net.Util;

namespace Velocity4Net.Runtime.Res.Loader
{

    /*
     * Licensed to the Apache Software Foundation (ASF) under one
     * or more contributor license agreements.  See the NOTICE file
     * distributed with this work for additional information
     * regarding copyright ownership.  The ASF licenses this file
     * to you under the Apache License, Version 2.0 (the
     * "License"); you may not use this file except in compliance
     * with the License.  You may obtain a copy of the License at
     *
     *   http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing,
     * software distributed under the License is distributed on an
     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     * KIND, either express or implied.  See the License for the
     * specific language governing permissions and limitations
     * under the License.
     */























    /**
     * A loader for templates stored on the file system.  Treats the template
     * as relative to the configured root path.  If the root path is empty
     * treats the template name as an absolute path.
     *
     * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
     * @author <a href="mailto:mailmur@yahoo.com">Aki Nieminen</a>
     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
     * @version $Id: FileResourceLoader.java 1866609 2019-09-08 10:42:47Z cbrisson $
     */
    public class FileResourceLoader : ResourceLoader
    {
        /**
         * The paths to search for templates.
         */
        private List<String> paths = new List<string>();

        /**
         * Used to map the path that a template was found on
         * so that we can properly check the modification
         * times of the files. This is synchronizedMap
         * instance.
         */
        private IDictionary<string, string> templatePaths = new ConcurrentDictionary<string, string>();

        /**
         * @see ResourceLoader#init(org.apache.velocity.util.ExtProperties)
         */
        public void init(ExtProperties configuration)
        {
            log.Info("FileResourceLoader: initialization starting.");

            paths.addAll(configuration.getVector(RuntimeConstants.RESOURCE_LOADER_PATHS));

            // trim spaces from all paths
            for (ListIterator<String> it = paths.listIterator(); it.hasNext();)
            {
                String path = StringUtils.trim(it.next());
                it.set(path);
                log.Debug("FileResourceLoader: adding path '{}'", path);
            }
            log.Info("FileResourceLoader: initialization complete.");
        }

        /**
         * Get a Reader so that the Runtime can build a
         * template with it.
         *
         * @param templateName name of template to get
         * @return Reader containing the template
         * @throws ResourceNotFoundException if template not found
         *         in the file template path.
         * @since 2.0
         */
        public TextReader getResourceReader(String templateName, String encoding)
        {
            /*
             * Make sure we have a valid templateName.
             */
            if (String.IsNullOrEmpty(templateName))
            {
                /*
                 * If we don't get a properly formed templateName then
                 * there's not much we can do. So we'll forget about
                 * trying to search any more paths for the template.
                 */
                throw new ResourceNotFoundException(
                        "Need to specify a file name or file path!");
            }

            String template = FilenameUtils.normalize(templateName, true);
            if (template == null || template.Length == 0)
            {
                String msg = "File resource error: argument " + template +
                        " contains .. and may be trying to access " +
                        "content outside of template root.  Rejected.";

                log.Error("FileResourceLoader: {}", msg);

                throw new ResourceNotFoundException(msg);
            }

            int size = paths.Count;
            foreach (String path in paths)
            {
                InputStream rawStream = null;
                Reader reader = null;

                try
                {
                    rawStream = findTemplate(path, template);
                    if (rawStream != null)
                    {
                        reader = buildReader(rawStream, encoding);
                    }
                }
                catch (IOException ioe)
                {
                    closeQuiet(rawStream);
                    String msg = "Exception while loading Template " + template;
                    log.Error(msg, ioe);
                    throw new VelocityException(msg, ioe, rsvc.getLogContext().getStackTrace());
                }
                if (reader != null)
                {
                    /*
                     * Store the path that this template came
                     * from so that we can check its modification
                     * time.
                     */
                    templatePaths.put(templateName, path);
                    return reader;
                }
            }

            /*
             * We have now searched all the paths for
             * templates and we didn't find anything so
             * throw an exception.
             */
            throw new ResourceNotFoundException("FileResourceLoader: cannot find " + template);
        }

        /**
         * Overrides superclass for better performance.
         * @since 1.6
         */
        public bool resourceExists(String name)
        {
            if (name == null)
            {
                return false;
            }
            name = FilenameUtils.normalize(name);
            if (name == null || name.Length == 0)
            {
                return false;
            }

            int size = paths.Count;
            foreach (String path in paths)
            {
                try
                {
                    File file = getFile(path, name);
                    if (file.canRead())
                    {
                        return true;
                    }
                }
                catch (Exception ioe)
                {
                    log.Debug($"Exception while checking for template {name}");
                }
            }
            return false;
        }

        /**
         * Try to find a template given a normalized path.
         *
         * @param path a normalized path
         * @param template name of template to find
         * @return InputStream input stream that will be parsed
         *
         */
        private Stream findTemplate(String path, String template)
        {
            try
            {
                File file = getFile(path, template);

                if (file.canRead())
                {
                    FileInputStream fis = null;
                    try
                    {
                        fis = new FileInputStream(file.getAbsolutePath());
                        return fis;
                    }
                    catch (IOException e)
                    {
                        closeQuiet(fis);
                        throw e;
                    }
                }
                else
                {
                    return null;
                }
            }
            catch (FileNotFoundException fnfe)
            {
                /*
                 *  log and convert to a general Velocity ResourceNotFoundException
                 */
                return null;
            }
        }

        private void closeQuiet(Stream _is)
        {
            if (_is != null)
            {
                try
                {
                    _is.Close();
                }
                catch (IOException ioe)
                {
                    // Ignore
                }
            }
        }

        /**
         * How to keep track of all the modified times
         * across the paths.  Note that a file might have
         * appeared in a directory which is earlier in the
         * path; so we should search the path and see if
         * the file we find that way is the same as the one
         * that we have cached.
         * @param resource
         * @return True if the source has been modified.
         */
        public bool isSourceModified(Resource resource)
        {
            /*
             * we assume that the file needs to be reloaded;
             * if we find the original file and it's unchanged,
             * then we'll flip this.
             */
            bool modified = true;

            String fileName = resource.getName();
            String path = (String)templatePaths.get(fileName);
            FileInfo currentFile = null;

            for (int i = 0; currentFile == null && i < paths.Count; i++)
            {
                String testPath = (String)paths.get(i);
                FileInfo testFile = getFile(testPath, fileName);
                if (testFile.Exists)
                {
                    currentFile = testFile;
                }
            }
            FileInfo file = getFile(path, fileName);
            if (currentFile == null || !file.Exists)
            {
                /*
                 * noop: if the file is missing now (either the cached
                 * file is gone, or the file can no longer be found)
                 * then we leave modified alone (it's set to true); a
                 * reload attempt will be done, which will either use
                 * a new template or fail with an appropriate message
                 * about how the file couldn't be found.
                 */
            }
            else if (currentFile.Equals(file) && file.canRead())
            {
                /*
                 * if only if currentFile is the same as file and
                 * file.lastModified() is the same as
                 * resource.getLastModified(), then we should use the
                 * cached version.
                 */
                modified = (file.lastModified() != resource.getLastModified());
            }

            /*
             * rsvc.Debug("isSourceModified for " + fileName + ": " + modified);
             */
            return modified;
        }

        /**
         * @see ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
         */
        public long getLastModified(Resource resource)
        {
            String path = (String)templatePaths.get(resource.getName());
            FileInfo file = getFile(path, resource.getName());

            if (file.Exists)
            {
                return file.LastWriteTime.Ticks;
            }
            else
            {
                return 0;
            }
        }


        /**
         * Create a File based on either a relative path if given, or absolute path otherwise
         */
        private FileInfo getFile(String path, String template)
        {

            FileInfo file = null;

            if ("".Equals(path))
            {
                file = new FileInfo(template);
            }
            else
            {
                /*
                 *  if a / leads off, then just nip that :)
                 */
                if (template.StartsWith("/"))
                {
                    template = template.Substring(1);
                }

                file = new FileInfo(Path.Combine(path, template));
            }

            return file;
        }
    }
}